home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
FishMarket 1.0
/
FishMarket v1.0.iso
/
fishies
/
051-075
/
disk_064
/
readmes
/
backgrnd.doc
< prev
next >
Wrap
Text File
|
1992-05-06
|
18KB
|
431 lines
BACKGROUND ON THE EXAMPLE IFF SOURCE CODE
Jerry Morrison, 1/30/86
The example IFF code is written using a programming style and techniques
that may be unfamiliar to you. So here's a tutorial on "call-back
procedures","enumerators", "interfaces", and "sub-classed structures". I
recommend these programming practices independently of IFF software.
DEFINITIONS: "CLIENT" VS. "USER"
First, some definitions. The word "user" is reserved for a human user of a
software package. That's you and me.
A "client" of a software package, on the other hand, is a piece of software
that uses that software package. A program that calls operating system
routines such as "OpenFile" is a client of that operating system.
CALL-BACK PROCEDURES
Consider an operating system subroutine "ListDir" that lists the files in a
disk directory. It might allow you to list just the filenames matching a
pattern like "a*.text". Maybe you can ask it to list just the files created
since yesterday ... or those longer than 2000 bytes. ListDir is a fancy,
general-purpose directory subroutine that lets you pass in a number of
arguments to filter the listing.
A C definition might look like:
void ListDir(directory, namePattern, minSize, maxSize, minDate ...); ... {
for (each file in the directory)
if ( PatternMatch(namePattern, filename)
&& fileSize >= minSize
&& fileSize <= maxSize
&& fileDate >= minDate
&& ... )
printf("%s\n", filename); /* probably fancier than this... */
}
and your call to it:
ListDir(myDir, "a*.text", 0, maxFileSize, date1_1_1900, ...);
When you think about it, these filtering arguments make up a
special-purpose "file filtering language". The person who designed this
subroutine "ListDir" might be pretty pleased with his accomplishment. But
in practice he can never put in enough features into this special-purpose
language to satisfy everyone. (You say you need to list just the files
currently open?) And he may have provided a lot of functionality that is
rarely needed. Is this filtering language what he should spending his time
designing, writing, and debugging?
A much better technique is to use a "call-back procedure". The concept is
simple: instead of all those filter arguments to ListDir, you pass it a
pointer to a "filter procedure". ListDir simply calls your procedure (via the
pointer) to do the filtering, once per file. It passes each filename to your
"filter proc", which returns "TRUE" to include that file in the listing or
"FALSE" to skip it.
typedef BOOL FilterProc(); /* FilterProc: a BOOL procedure */
void ListDir(directory, filterProc);
Directory directory; FilterProc *filterProc; {
for (each file in the directory)
if ( (*filterProc)(filename) ) printf("%s\n", filename);
}
and your code:
BOOL MyFilterProc(filename) STRING filename; {
return(PatternMatch("a*.text", filename));
}
...
ListDir(myDir, MyFilterProc);
This technique has many advantages. It gives unlimited flexibility to
ListProc. It means you can use a general-purpose programming language
instead of learning a special-purpose filtering language. It's more
efficient to call a compiled subroutine than to "interpret" the filtering
parameters. And it means you can do anything you want in a filter proc,
from selecting files on the basis of numerology to copying files to backup
tape.
In practice, ListDir would have data about each file readily available. So it
should pass this data to the filter proc to save time.
As Alan Kay once said, "Simple things should be simple and complex things
should be possible."
STANDARD CALL-BACK PROCEDURE
I could extend ListDir to accept a NULL FilterProc pointer to mean "list all
files". More likely, I'd supply a standard call-back procedure "FilterTRUE"
that always returns TRUE. Then ListDir(directory, FilterTRUE) will list all
files with no special test for filterProc == NULL.
BOOL FilterTRUE(filename) STRING filename; {
return(TRUE);
}
ENUMERATORS
Let's take our ListDir example one step further. Rather than have ListDir
print the selected filenames, have it JUST call your custom proc for every
file. Let your custom proc print the filenames, maybe in your own personal
format. Or maybe have it quietly backup new files, or ask the user which
ones to delete, or ...
typedef CallBackProc(/* filename */);
void ListDir(directory, callBackProc);
Directory directory; CallBackProc *callBackProc; {
for (each file in the directory)
(*callBackProc)(filename);
}
and your code:
void MyProc(filename) STRING filename; {
if ( PatternMatch("a*.text", filename) )
printf("%s\n", filename);
}
...
ListDir(myDir, MyProc);
Now we're talking about a full-blown "enumerator". The procedure "ListDir"
is said to "enumerate" all the files in a directory. It "applies" your
call-back procedure to each file. The enumerator scans the directory and
your call-back procedure processes the files. It deals with the internal
directory details and you deal with the printout. A nice separation of
concerns.
ListDir should come with a standard call-back procedure "PrintFilename"
that lists the filename. By simply passing PrintFilename to ListDir, you
can print a directory. By writing a call-back procedure that selectively
calls the PrintFilename, you can filter the listing.
void PrintFilename(filename) STRING filename; {
printf("%s\n", filename);
}
ENUMERATION CONTROL
A simple enhancement is to empower the call-back procedure to stop the
enumeration early. That's easy. Have it return "TRUE" to stop. This is very
handy, for example, to quit when you find what you're looking for. Let's
expand this boolean "continue/stop" result into an integer error code.
#define OKAY 0
#define DONE -1
typedef int CallBackProc(/* filename */);
int ListDir(directory, callBackProc);
Directory directory; CallBackProc *callBackProc; {
int result = OKAY;
for (each file in the directory) while (result == OKAY)
result = (*callBackProc)(filename);
return(result);
}
IFF FILE ENUMERATOR
Now we'll relate these techniques to the example IFF code. I'm assuming
that you've read "EA IFF 85" Standard for Interchange Format Files. That
memo is available from Commodore as part of their Amiga documentation.
Also ask Commodore for "ILBM" IFF Interleaved Bitmap and the example IFF
source code.
Two things make IFF files very flexible for lots of interchange between
programs. First, file formats are independent of RAM formats. That means
you have to do some conversion when you read and write IFF files. Second,
the contents are stored in chunks according to global rules. That means you
have to parse the file, i.e. scan it and react to what's actually there.
In the example IFF files IFF.H and IFFR.C, the routines ReadIFF, ReadIList, &
ReadICat are enumeration procedures. ReadIFF scans an IFF file,
enumerating all the "FORM", "LIST", "PROP", and "CAT" chunks encountered.
ReadIList & ReadICat enumerate all the chunks in a LIST and CAT,
respectively.
A ClientFrame record is a bundle of pointers to 4 "call-back procedures"
getList, getProp, getForm, and getCat. These 4 procedures are called by
ReadIFF, ReadIList, and ReadICat when the 4 kinds of IFF "groups" are
encountered: "LIST", "PROP", "FORM", or "CAT".
These 3 enumerator procedures and 4 client procedures together make up a
reader for IFF files--a very simple recursive descent parser. If you want
to learn more about parsing, a real good place to look is the new edition
"dragon book" by Aho, Ullman, and Sethi.
The procedure "SkipGroup" is just a default call-back procedure.
The "IFFP" values IFF_OKAY through BAD_IFF are the error codes used by
the IFF enumerators. We use the type "IFFP" to declare variables (and
procedure results) that hold such values. The code "IFF_OKAY" means "AOK;
keep enumerating". The other values mean "stop" for one reason or other.
"IFF_DONE" means "we're all done", while "END_MARK" means "we hit the
end at this nesting level".
CALL-BACK PROCEDURE STATE
ListDir is an enumerator with some internal state--it internally
remembers its place in the directory. It loops over the directory, calling
the client proc once per file. That's fine for some cases and less
convenient for others. Consider this example that just lists the first 10
files:
int count;
int PrintFirst10(filename) STRING filename; {
if (++count > 10) return(DONE);
printf("%s\n", filename);
return(OKAY);
}
void DoIt(); {
...
count = 0;
ListDir(myDir, PrintFirst10);
...
}
Inherently, the client's code has to be split into code that calls the
enumerator and a call-back procedure. Thus any communication between
the two must be via global variables. In this trivial example, the global
"count" saves state data between calls to PrintFirst10. Often, it's much
more complex. But globals won't work if you need reenterent or recursive
code. We really want "count" to be a local variable of DoIt.
Fixing this in Pascal is easy: Define PrintFirst10 as a nested procedure
within DoIt so it can access DoIt's local variables. The manual analog in C
is to redefine the enumerator to pass a raw "client data pointer" straight
through to the call-back procedure. The two client procedures then
communicate through the "client data pointer". DoIt would call
ListDir(myDir, PrintFirst10, &count) which calls PrintFirst10(filename,
&count).
#define OKAY 0
#define DONE -1
typedef int CallBackProc(/* filename, clientData */);
int ListDir(directory, callBackProc, clientData);
Directory directory; CallBackProc *callBackProc; BYTE *clientData; {
int result = OKAY;
for (each file in the directory) while (result == OKAY)
result = (*callBackProc)(filename, clientData);
return(result);
}
In general, an enumerator is sometimes inconvenient because it takes over
control. Think about this: How could you enumerate two directories in
parallel and copy the newer files from one directory to the other?
STATELESS ENUMERATOR
An alternate form without this disadvantage is the "stateless enumerator".
In a stateless enumerator, it's up to the client to keep its place in the
enumeration. Call a procedure like GetNextFilename each time around the
loop.
STRING curFilename = NULL;
int count = 0;
do {
if (++count > 10) break; /* stop after 10 files */
curFilename = GetNextFilename(directory, curFilename);
if (curFilename == NULL) break; /* stop at end of directory */
printf("%s\n", filename);
}
The stateless enumerator is sometimes better because it puts the client
in control. The above example shows how easy it is to keep state
information between iterations and to stop the enumeration easy. It's also
easy to do things like list two directories in parallel.
IFF CHUNK ENUMERATOR
The following IFFR.C routines make up a stateless IFF chunk enumerator:
OpenRIFF, OpenRGroup, GetChunkHdr and CloseRGroup. Together with
IFFReadBytes, we have a complete layer of "chunk reader" subroutines.
These subroutines are built upon the file stream package in the local
system library.
GetChunkHdr is the "get next" procedure you call to get the next IFF chunk.
(GetFChunkHdr, GetF1ChunkHdr, and GetPChunkHdr are subroutines that call
GetChunkHdr and do a little extra work.) OpenRIFF and OpenRGroup do the
initialization needed before you can call GetChunkHdr. CloseRGroup does
the cleanup work.
You supply a "GroupContext" pointer each time you call one of these "chunk
reader" procedures. The enumeration state is kept in a GroupContext record
which the *client* must allocate but the *enumerator* routines initialize
and maintain. (The client may peek into a GroupContext but should never
modify it directly.) The two procedures OpenRIFF and OpenRGroup initialize
the GroupContext record. This "opens a context" for reading chunks. The
procedure CloseRGroup cleans up when you're done with a GroupContext.
Here's the essense of an IFF scanner program. It handles whatever it finds,
unlike inflexible file readers that demand conformance to a rigid file
format. [Note: This code doesn't check for errors or end-of-context.]
OpenRGroup(..., context); /* initialize */
do {
id = GetChunkHdr(context); /* get the next chunk's ID */
switch (id) {
case AAAA: {read in an AAAA chunk; break};
case BBBB: {read in a BBBB chunk; break};
...
default: {}; /* just ignore unrecognized chunks */
}
CloseRGroup(context); /* cleanup */
GetChunkHdr reads the next chunk header and returns its chunk ID. You then
dispatch on the chunk ID, that is, switch to a different piece of code for
each type of chunk. If you don't recognize the chunk ID, just keep looping.
In each "case:" statement, call IFFReadBytes one or more times to read the
chunk's contents. The readin work you do here depends on the chunk type
and what you need in RAM. Since GetChunkHdr automatically skips to the
start of the next chunk, it doesn't matter if you don't read all the data
bytes.
GetChunkHdr does some other things for you automatically. When it reads a
"group" chunk header (a chunk of type "FORM", "LIST", "CAT ", or "PROP") it
automatically reads the subtype ID. That makes it very convenient to just
open the contents of the group chunk as a group context and read the
nested chunks. See the example source program ShowILBM for more about
the relationship between a "GroupContext" and a "ClientFrame".
Like all the example IFF code, GetChunkHdr checks for errors. To handle
GetChunkHdr errors, we just add cases to the switch statment. To stop at
end-of-context or an error in a switch case, we add a "while" clause at the
end of the "do" statement.
CLIENTS, INTERFACES, AND IMPLEMENTORS
In the ListDir example, you can see that a lot of flexibility comes from
decoupling the task of tracing through the directory's data structures from
the task of filtering files and printing filenames. This is called
modularity, or simply, dividing a program into parts.
Choosing good module boundaries is an art. It has a big impact on a
programmer's ability to coope with lrge programs. Good modularity makes
programs much easier to understand and modify. But this topic would be
another whole tutorial in itself.
Just be aware that the example IFF program is divided into various
"modules", each of which implements a different part of the bigger picture.
One such module is the low level IFF reader/writer. It's split into two
files IFFR.C and IFFW.C. Other such modules are the run encoder/decoder
Packer.C and UnPacker.C, and ILBM read/write subroutines ILBMR.C and
ILBMW.C.
You'll notice that all three of these "modules" are split into a pair of files.
That's because most linkers aren't fancy enough to automatically eliminate
unused subroutines, e.g. for a program like ShowILBM that reads but doesn't
need the writer code. Also, a program like DeluxePaint wants read and
write code in separate overlays. So think of each pair as a single module.
What I want to point out is the basic structure. Each "module" has an
"interface" file (a .H file) that separates the "implementor" .C file(s) from
the "client" programs. This interface is very important, in fact, more
important than the code details inside the .C files. The interfaces for the
above-mentioned modules are called IFF.H, Packer.H, and ILBM.H.
Everything about a layer of software that the clients need to know belongs
in its interface: constant and type definitions, extern declarations for the
procedures, and comments. The comments detail the purpose of the module
and each procedure, the procedure arguments, side effects, results, and
error codes, etc. Nothing the clients don't need to know belongs in its
interface: internal implementation details that might change.
Thus, the modularization and other important design information is
collected and documented in these interface files. So if you want to
understand what a module does and how to use it, READ ITS INTERFACE.
Don't dive headfirst into the implementation.
Two of the original articles on modular programming are
D.L. Parnas, "On the Criteria To Be Used in Decomposing Systems into
Modules". Communications of the ACM 15, 12 (Dec. '72), pp 1053-1058.
B. Liskov and S. Zilles, "Programming with Abstract Data Types".
Proceedings ACM SIGPLAN Conference on Very High-Level Languages.
SIGPLAN Notices 9, 4 (April '74), pp 50-59.
SUBCLASSED STRUCTURES
One more technique. In programming, a general-purpose module may define
a structure like ClientFrame. Along comes a more special-purpose program
that needs a structure like it but with specialized fields added on. The
answer is to build a larger structure whose first field is the earlier
structure. This is called "subclassing" a structure, a term that comes from
subclassing in Smalltalk.
In the Macintosh(tm) toolbox, the record GrafPort is subclassed to produce
the record WindowRecord, which is subclassed again to produce a
DialogWindow record.
Similarly in the example IFF program ShowILBM, the structure ClientFrame
is subclassed to produce the more specialized structure ILBMFrame.
typedef struct {
ClientFrame clientFrame;
UBYTE foundBMHD;
...
} ILBMFrame;
Since the first field of an ILBMFrame is a ClientFrame, the ShowILBM
procedure ReadPicture can coerce a *ClientFrame pointer to an
*ILBMFrame pointer to pass it to ReadIFF (which knows nothing about
ILBMFrame). When ReadIFF calls back ShowILBM's getForm procedure, we
can coerce it back to an *ILBMFrame pointer. Take a look at ShowILBM to
see how this works.